延續 Day 25 完成的醫師查詢與每週門診時間表串接,今天開始開發智慧醫院管理平台的會員登入與註冊功能。
在前面的開發過程中,我已經使用 Figma 設計登入與註冊頁面,並將 HTML、CSS 轉換為 Angular Component。
目前登入與註冊頁已經有基本的表單欄位、按鈕與 RWD 排版,但原本只有畫面,尚未真正連接後端資料庫。
例如使用者點擊登入按鈕時,原本只會顯示「尚未提供帳號驗證」,而註冊頁也沒有真正建立會員帳號。
因此,今天希望將原本的 Demo 登入與註冊頁,改成能透過 ASP.NET Core Web API 操作 Oracle Database 的會員系統。
USERS 與 PATIENTS 資料表。這次主要以完成前後端登入驗證為目標,保留之前已完成的登入、註冊 HTML、CSS 與 RWD,不重新設計畫面。
正式實作 API 之前,我先檢查 Angular 專案中的登入與註冊相關程式。
目前兩個頁面共用 AuthPage Component,透過 Angular Router 區分不同模式:
/login
↓
AuthPage:登入模式
/register
↓
AuthPage:註冊模式
這次沿用 Standalone Components、Reactive Forms、signals 與 OnPush 的設計。
目前登入頁包含:
原本登入頁主要負責呈現畫面,沒有真正向 ASP.NET Core 發送登入請求。
註冊頁原本包含:
| 欄位 | 原本用途 |
|---|---|
| 姓名 | 輸入使用者姓名 |
| 身分證字號 | 個人身分資料 |
| 出生日期 | 個人基本資料 |
| 手機 | 聯絡方式 |
| 帳號 | 登入使用的帳號 |
| 密碼 | 設定登入密碼 |
| 確認密碼 | 再次輸入密碼 |
| 同意條款 | 確認使用者同意 |
這次檢查時發現,雖然部分欄位已經顯示輸入格式提示,但實際上還沒有完整的表單驗證。
例如帳號提示需要 6–20 位英數字元,但提示文字本身並不會阻止錯誤資料送出。
因此,後續需要加入 Angular Reactive Forms 驗證。
除了登入頁,專案原本還有 SessionService,主要用來支援 Demo 會員及展示功能。
但這套機制並不是正式的後端驗證。
因此,這次沒有直接沿用原本的 Demo 密碼判斷方式,而是另外建立正式 AuthService。
另外,原本的「我的預約」與預約詳細頁仍使用 Demo 資料,因此這次也需要避免正式登入的會員看到展示資料後,誤以為那是自己的預約紀錄。
完成現有功能分析後,接著開始建立會員相關資料表。
原本 Day 22 已經規劃過部分 Oracle 資料庫結構,因此這次先檢查既有 SQL 草稿,再建立獨立的 Day 26 建表腳本。
這次沒有重新設計整個資料庫,而是先建立登入與註冊需要的兩張資料表:
USERS:保存會員登入帳號。PATIENTS:保存與會員對應的病人基本資料。USERS 主要負責保存帳號、密碼雜湊與帳號狀態。
| 欄位 | Oracle 型別 | 用途 |
|---|---|---|
| USER_ID | NUMBER(12,0) | 會員 ID |
| USERNAME | VARCHAR2(20 CHAR) | 登入帳號 |
| VARCHAR2(254 CHAR) | Email,目前不使用 | |
| PHONE | VARCHAR2(30 CHAR) | 聯絡手機 |
| PASSWORD_HASH | VARCHAR2(512 BYTE) | 密碼雜湊 |
| CREATED_AT | TIMESTAMP WITH TIME ZONE | 建立時間 |
| ACCOUNT_STATUS | VARCHAR2(20 CHAR) | 帳號狀態 |
其中,最重要的欄位是 PASSWORD_HASH。
這次不在資料庫保存明文密碼,而是由 ASP.NET Core 將密碼轉換成安全的雜湊結果,再存入 Oracle。
另外,USERNAME 設定 UNIQUE,避免使用者註冊重複帳號。
PATIENTS 則保存與會員對應的病人資料。
| 欄位 | Oracle 型別 | 用途 |
|---|---|---|
| PATIENT_ID | NUMBER(12,0) | 病人 ID |
| USER_ID | NUMBER(12,0) | 對應 USERS |
| PATIENT_NAME | VARCHAR2(100 CHAR) | 病人姓名 |
這次使用外鍵建立關聯:
USERS
│
└── USER_ID
│
▼
PATIENTS
│
└── PATIENT_ID
PATIENTS.USER_ID 同時具有 FOREIGN KEY 與 UNIQUE 限制,讓一個會員最多對應一筆病人資料。
不過,FOREIGN KEY 與 UNIQUE 並不能保證每個會員一定有病人資料,因此後續註冊 API 還需要使用 Transaction 確保兩張表一起建立成功。
之前醫師資料使用手動指定的測試 ID,但正式會員註冊不能讓前端自行決定使用者 ID。
因此這次使用:
user_id NUMBER(12,0)
GENERATED ALWAYS AS IDENTITY
GENERATED ALWAYS AS IDENTITY 表示主鍵由 Oracle 自動產生。
這樣就不需要在註冊 API 中自行計算下一個 ID,也能避免使用 MAX(id)+1 時發生多人同時註冊的衝突問題。
PATIENTS.PATIENT_ID 也使用相同方式。
第一次在 SQL Developer 建立 USERS 時,遇到:
ORA-01031: insufficient privileges
一開始我以為是 CREATE TABLE 語法有問題,但檢查 Oracle 權限後發現,目前的 SMARTCARE_DEV 雖然有 CREATE TABLE,卻沒有 CREATE SEQUENCE。
這次與之前建立醫師排班表不同,因為新的資料表使用 Identity 自動產生 ID。
後來確認權限問題並補足必要權限後,重新執行 SQL,才成功建立兩張資料表。
這次在 SQL Developer 使用 SMARTCARE_DEV,成功建立:
Table USERS created.
Table PATIENTS created.
接著檢查 Identity:
| TABLE_NAME | COLUMN_NAME | GENERATION_TYPE |
|---|---|---|
| USERS | USER_ID | ALWAYS |
| PATIENTS | PATIENT_ID | ALWAYS |
另外,資料表的 PK、FK、UNIQUE 與 CHECK 限制都顯示:
ENABLED
VALIDATED
建表完成後,兩張資料表一開始都是 0 筆。
這次也學到,Oracle 的 CREATE TABLE 屬於 DDL,成功執行後會隱含 COMMIT,不需要另外手動提交建表操作。
USERS、PATIENTS 資料表

完成 Oracle 資料表後,開始建立 ASP.NET Core 註冊 API。
這次新增的主要檔案包括:
backend/SmartCare.Api/
├── Controllers/
│ └── AuthController.cs
├── Dtos/
│ └── AuthDtos.cs
├── Auth/
│ ├── AuthModels.cs
│ ├── AuthValidation.cs
│ ├── AuthService.cs
│ └── AuthSecurity.cs
└── Data/
├── UserRepository.cs
└── RegistrationTransaction.cs
這次沒有將帳號相關查詢放進原本的 DoctorRepository,而是另外建立 UserRepository,讓醫師資料與會員資料分開管理。
新增:
POST /api/auth/register
註冊時,Angular 會提供:
{
"username": "smartcare01",
"password": "ExamplePassword123!",
"confirmPassword": "ExamplePassword123!",
"displayName": "虛構測試會員",
"phone": null,
"acceptTerms": true
}
以上是 API 格式示意,不是實際測試帳號或密碼。
這次註冊 API 主要檢查:
通過驗證後,才開始新增資料。
這次使用 ASP.NET Core 的 PasswordHasher 處理密碼。
核心概念是:
var hashedPassword = passwordHasher.HashPassword(
user,
password
);
以上是主要操作方式示意。
HashPassword() 會產生包含必要雜湊資訊的結果,再將結果保存於 Oracle 的 PASSWORD_HASH。
登入時則使用:
var result = passwordHasher.VerifyHashedPassword(
user,
storedPasswordHash,
password
);
這樣後端不需要,也不應該透過讀取明文密碼來比較登入資料。
這次最重要的新觀念是 Transaction(交易)。
會員註冊時,需要同時建立:
如果第一筆成功、第二筆失敗,卻沒有回復交易,就會留下只有會員帳號、沒有病人資料的不完整紀錄。
因此這次採用:
收到註冊資料
↓
驗證欄位
↓
PasswordHasher
↓
開始 Oracle Transaction
↓
INSERT USERS
↓
取得 USER_ID
↓
INSERT PATIENTS
↓
兩筆都成功?
┌───┴───┐
│ │
是 否
│ │
COMMIT ROLLBACK
新增 USERS 後,利用 Oracle RETURNING 取得 Identity 產生的 USER_ID,再將這個 ID 寫入 PATIENTS。
兩筆新增操作使用相同的 OracleConnection 與 OracleTransaction,成功才提交,失敗就回復。
為了確認交易真的有效,這次也執行了一個預期失敗的測試。
測試中故意讓第二筆 PATIENTS 寫入觸發:
ORA-01400
代表必要欄位不能為 NULL。
結果確認交易 Rollback 後:
這次讓我理解到,Transaction 不只是資料庫語法,也是在保護使用者資料的一致性。
完成註冊後,下一步是讓使用者能夠登入,並在後續頁面保持登入狀態。
這次新增:
POST /api/auth/login
GET /api/auth/me
POST /api/auth/logout
登入時由 Angular 傳送:
{
"username": "smartcare01",
"password": "ExamplePassword123!",
"rememberMe": false
}
後端會依序:
如果帳號不存在或密碼錯誤,都使用相同的登入失敗訊息,避免直接透露帳號是否存在。
這次評估後,選擇 ASP.NET Core Cookie Authentication 作為目前 Angular 網站的登入機制。
主要原因是這個專案目前以單一網站為主,Angular 與 API 使用相對 /api 路徑,適合由瀏覽器管理驗證 Cookie。
登入成功後,後端透過 Set-Cookie 建立登入狀態。
Angular 不需要將驗證憑證放進 localStorage 或 sessionStorage。
這次 Cookie 的主要設定包括:
| 設定 | 用途 |
|---|---|
| HttpOnly | 降低 JavaScript 直接讀取驗證 Cookie 的風險 |
| SameSite=Lax | 限制部分跨站請求攜帶 Cookie |
| 有效期限 20 分鐘 | 控制登入狀態的存續時間 |
| 不滑動延長 | 不因每次請求而自動延長期限 |
| Secure | 非 Development 環境要求安全傳輸 |
目前「記住我」尚未啟用永久登入。
新增:
GET /api/auth/me
這支 API 用來查詢目前已登入的會員。
例如成功回應:
{
"data": {
"id": 1,
"username": "smartcare01",
"displayName": "虛構測試會員",
"patientId": 1
}
}
回傳資料不包含密碼及密碼雜湊。
這支 API 的用途是讓 Angular 在重新整理頁面後,重新確認目前使用者是否仍有有效的登入狀態。
新增:
POST /api/auth/logout
使用者登出時,後端清除目前瀏覽器的驗證 Cookie。
正常情況下,登出後再次呼叫 /api/auth/me,應該得到 HTTP 401。
不過目前的登出機制只處理這個瀏覽器的 Cookie,尚未實作其他裝置或已複製票證的即時撤銷。
完成 Cookie Authentication 後,接著要處理另一個重要的問題:CSRF。
CSRF(Cross-Site Request Forgery,跨站請求偽造)是一種利用瀏覽器自動攜帶驗證資訊的特性,誘使使用者在已登入的狀態下發送非預期請求的攻擊方式。
由於這次使用 Cookie Authentication,因此不能只完成登入 Cookie,還需要搭配 CSRF 防護。
後端新增:
GET /api/auth/csrf
這次的流程是:
Angular 準備送出註冊/登入
↓
GET /api/auth/csrf
↓
後端提供 CSRF Token
↓
Angular 發送 POST
↓
X-XSRF-TOKEN Header
↓
ASP.NET Core 驗證 Token
↓
執行註冊/登入/登出
Angular 的 HttpClient 具備 XSRF 支援,後端則使用 ASP.NET Core IAntiforgery 驗證。
這次主要使用:
XSRF-TOKEN:供 Angular 使用的 CSRF Cookie。X-XSRF-TOKEN:POST 請求所附加的 Header。這兩者是 CSRF 防護的一部分,與 HttpOnly 的登入驗證 Cookie 不同。
為了避免短時間大量嘗試登入與註冊,這次後端也加入 Rate Limiting。
目前設定:
| API | 限制 |
|---|---|
| 登入 | 每個來源 IP 每分鐘 5 次 |
| 註冊 | 每個來源 IP 每 10 分鐘 3 次 |
超過限制時,回傳:
HTTP 429 Too Many Requests
並提供 Retry-After 資訊。
這次的限流主要適合目前單一開發程序使用,正式部署若有多台伺服器,還需要進一步規劃。
後端 API 完成後,開始進行 Angular 前端串接。
這次沒有把 HTTP 請求直接寫在登入頁中,而是另外建立 AuthService,集中管理註冊、登入、查詢目前會員及登出。
主要新增:
src/app/core/
├── auth-api.models.ts
├── auth.service.ts
└── auth-validation.ts
註冊 Request 與登入 Request 分開定義,避免直接把整個 Reactive Forms 內容傳到後端。
例如會員回傳資料:
export interface AuthUser {
id: number;
username: string;
displayName: string | null;
patientId: number | null;
}
這樣前端可以使用明確的欄位名稱與型別。
AuthService 使用既有的 HttpClient,透過相對路徑:
/api/auth
呼叫後端。
主要方法包括:
register()
login()
getCurrentUser()
logout()
另外也包含取得 CSRF Token 與處理登入狀態的功能。
這樣登入頁、Header 與 Route Guard 不需要各自重新撰寫 HTTP 請求。
這次比較重要的設計,是將登入狀態分成四種:
export type AuthState =
| { status: 'checking' }
| { status: 'authenticated'; user: AuthUser }
| { status: 'anonymous' }
| { status: 'error' };
這四種狀態分別代表:
| 狀態 | 意義 |
|---|---|
| checking | 正在確認登入狀態 |
| authenticated | 已登入 |
| anonymous | 尚未登入 |
| error | 無法完成身分查詢 |
例如 Angular 啟動時呼叫 /api/auth/me:
這樣可以避免 API 暫時無法使用時,前端直接把使用者當成正常登出。
完成 AuthService 後,接著修改原本登入與註冊頁的表單功能。
這次主要調整 auth.ts 與 auth.html,沒有重新修改原本的 CSS 排版。
原本登入頁已有帳號與密碼欄位。
這次主要新增:
另外,原本帳號欄的 placeholder 誤寫為「請輸入姓名」,也在這次調整成符合實際用途的提示。
註冊頁加入 Reactive Forms 驗證。
目前主要規則:
| 欄位 | 驗證方式 |
|---|---|
| 姓名 | 必填,最多 100 字元 |
| 帳號 | 6–20 位英數字元 |
| 密碼 | 15–128 個 Unicode code point |
| 確認密碼 | 必須與密碼相同 |
| 手機 | 選填,有值時需符合台灣手機格式 |
| 同意條款 | 必須勾選 |
| 身分證、生日 | 暫不蒐集 |
這次特別注意密碼長度的計算方式。
JavaScript 字串的 length 計算的是 UTF-16 code units,而後端這次採用 Unicode code point 的長度規則。
如果前後端使用不同的計算方式,就可能產生「前端允許、後端拒絕」的情況。
因此這次另外建立共用驗證工具,讓 Angular 與後端使用一致的長度規則。
原本註冊畫面包含身分證與生日,但這次的後端資料表尚未規劃保存這兩項資料。
因此,目前先將欄位停用,不蒐集或傳送。
正式發送註冊請求時,只建立後端需要的欄位:
const request = {
username,
password,
confirmPassword,
displayName,
phone,
acceptTerms,
};
這樣可以避免不必要的資料被送到 API。
目前設計為:
填寫註冊資料
↓
Angular 表單驗證
↓
GET /api/auth/csrf
↓
POST /api/auth/register
↓
Oracle 建立會員
↓
顯示註冊結果
↓
前往登入頁
↓
POST /api/auth/login
↓
建立 Cookie
↓
進入會員頁
這次註冊成功後不會自動登入,使用者仍需要使用剛建立的帳號與密碼登入。
Angular 註冊頁與表單驗證

完成登入與註冊後,接著需要讓網站其他頁面知道目前的登入狀態。
原本 Header 使用 Demo SessionService 判斷使用者是否登入。
這次改為使用正式 AuthService 的登入狀態。
例如:
| AuthState | Header 顯示 |
|---|---|
| checking | 正在確認登入狀態 |
| anonymous | 登入入口 |
| authenticated | 會員名稱或會員操作 |
| error | 身分查詢錯誤或重試入口 |
這樣 Angular 不需要依賴 sessionStorage 中的 Demo 身分判斷正式會員。
這次也調整 Auth Guard,讓需要登入的頁面先確認目前會員狀態。
例如使用者尚未登入就進入:
/profile
系統會導向登入頁。
登入成功後,再依照安全的 returnUrl 返回站內頁面。
這次也限制 returnUrl 只能使用允許的站內路徑,避免登入後被導向不受信任的外部網站。
這次還遇到一個重要問題:
原本的個人資料頁與預約頁,顯示的不是正式會員資料,而是固定 Demo 內容。
如果直接把這些頁面接到新的登入功能,可能會造成使用者誤以為展示資料就是自己的個人資料或預約紀錄。
因此這次調整為:
/profile:顯示正式登入會員的資料。/demo/profile:保留原本展示用個人資料。目前登入功能雖然完成,但「我的預約」仍沒有串接正式預約 API。
登入成功畫面

完成後端及前端功能後,這次分別使用 Mock、真實 Oracle HTTP、Angular 單元測試與瀏覽器進行驗證。
本次後端測試結果:
| 測試類型 | 實際結果 |
|---|---|
| Mock 資料層與安全元件測試 | 304/304 通過 |
| 真實 Oracle 醫師 API 回歸 | 72/72 通過 |
| Program 啟動與端點 | 27/27 通過 |
| 真實 Oracle Auth HTTP 測試 | 91/91 通過 |
| Oracle Rollback 補驗 | 7/7 通過 |
| 後端 Build | 0 警告、0 錯誤 |
這些數字是通過的斷言數,不代表相同數量的獨立業務案例。
其中,停用帳號、部分資料庫故障等情境主要由 Mock 測試驗證,沒有刻意中斷 Oracle。
完成 Angular AuthService 與畫面串接後,執行完整測試。
| 項目 | 結果 |
|---|---|
| Angular 單元測試 | 163/163 通過 |
| 測試檔案數 | 21 |
| TypeScript 應用程式檢查 | 通過 |
| TypeScript 測試檢查 | 通過 |
| Production Build | 成功,無警告/錯誤 |
| 登入/註冊可執行無障礙規則 | 通過,未涵蓋色彩對比 |
這次也確認,原本的醫師列表、搜尋、詳細頁與每週門診時間表沒有因新增 AuthService 而受到影響。
完成 Angular 串接後,我在瀏覽器實際操作註冊頁。
一開始填寫資料並點擊「建立帳號」後,沒有看到明顯的成功訊息,也沒有在 Network 中找到 register 請求。
因此我先使用 SQL Developer 查詢 USERS,確認是否真的有新增帳號。
結果發現資料庫已經出現透過 Angular 註冊的新測試帳號:
syy000
syy001
這表示註冊請求確實曾成功建立會員資料。
不過,當時沒有取得對應的 Network HTTP 201 紀錄,因此不能只根據這次畫面操作宣稱已經親眼確認該次請求的狀態碼。
這也讓我學到:當畫面沒有明顯反應時,不能直接判斷 API 失敗,需要從瀏覽器及資料庫兩邊確認。
接著使用新註冊的測試帳號登入。
登入後,頁面顯示登入成功,進入 /profile 也能看到對應的會員資料。
另外,我在 /profile 重新整理頁面,確認登入狀態仍然保留。
這表示 Angular 已經可以透過後端的 Cookie 與會員身分查詢機制,在頁面重新整理後維持登入狀態。
但當時 Network 沒有保存到 login 請求的完整紀錄,因此這次主要以實際畫面與會員狀態作為瀏覽器驗證證據。
測試過程中,還遇到一個操作問題。
原本想查看登入及登出的 HTTP 狀態,但 Chrome DevTools 的 Network 沒有顯示請求。
後來才發現,篩選欄還保留之前測試醫師 API 時輸入的:
doctors
因此 Network 只顯示符合這個條件的請求,login、logout 與 me 都被篩選掉了。
清除篩選欄後,才能正常查看其他 API 請求。
這次也讓我學到,使用 DevTools 時不只要確認 Network 有沒有開啟,也要注意是否保留了之前的篩選條件。
這次已完成註冊資料庫確認、實際登入及重新整理後維持登入。
不過截至本文整理時,尚未取得登出後 /api/auth/me 回傳 401 的最終瀏覽器驗證紀錄。
因此,這部分仍列為待確認,不能直接寫成真實瀏覽器全部驗證完成。
這次開發與之前的醫師查詢 API 很不一樣。
Day 25 主要是讓 Angular 透過 GET 請求讀取 Oracle 資料,而 Day 26 開始需要處理會員新增、密碼安全及登入驗證。
Oracle Database
ASP.NET Core
Angular
這次使用 Codex 協助檢查登入/註冊頁與原有 Demo SessionService、規劃 Oracle 資料表、建立 ASP.NET Core Auth API,以及串接 Angular AuthService。
但這次沒有直接讓 Codex 一次修改整個專案,而是依序進行:
檢查原有登入/註冊頁
↓
分析 Day 22 Oracle 設計
↓
準備 USERS/PATIENTS SQL
↓
SQL Developer 手動建表
↓
ASP.NET Core 註冊 API
↓
PasswordHasher、Cookie、CSRF
↓
後端測試
↓
Angular AuthService
↓
Reactive Forms 串接
↓
Header/Guard/Profile
↓
單元測試與瀏覽器驗證
每個階段完成後,再確認程式與測試結果,避免一次修改太多地方而難以追蹤問題。
其中 Oracle 建表、權限確認及註冊資料查詢,也透過 SQL Developer 實際操作。
這次最大的收穫,是逐漸理解「登入成功」不只是前端顯示成功訊息,而是包含帳號驗證、密碼雜湊、Cookie、CSRF、防止未授權存取,以及前後端狀態一致性。
今天已將原本只有 Demo 功能的登入與註冊頁,逐步整合成由 ASP.NET Core 與 Oracle 支援的會員驗證功能。
主要完成:
目前仍有以下項目需要後續處理:
/me 回傳 401 的最終瀏覽器確認。Day 26 已完成本階段的登入與註冊 API 開發,接下來會先補齊最後的登出瀏覽器驗證,再繼續規劃智慧醫院預約網站的其他功能。
後續可以利用目前已建立的會員身分,進一步設計正式預約掛號流程。
不過在串接預約功能前,仍需要先確認日期化門診診次、預約名額,以及會員與預約紀錄之間的資料庫關聯。
這次最大的收穫,是從前一天單純讀取 Oracle 醫師資料,進一步學習如何將 Angular 表單、ASP.NET Core 身分驗證與 Oracle 交易整合起來。
也更清楚理解,真正的會員登入系統不只是驗證帳號密碼,還需要考慮資料一致性、安全性、錯誤處理、登入狀態及既有 Demo 功能的相容性。
這是 Day 26 最重要的開發成果。